﻿namespace Suture
{
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    using Suture.Strategies;
    using Suture.StubGeneration;

    public static class Builder
    {
        private static readonly IStubGenerator StubGenerator;

        static Builder()
        {
            var stubGeneratorType =
                AppDomain.CurrentDomain.GetAssemblies()
                         .SelectMany(assembly => assembly.GetTypes())
                         .Where(type => typeof(IStubGenerator).IsAssignableFrom(type))
                         .Where(type => !type.IsInterface)
                         .FirstOrDefault(type => type != typeof(DefaultStubGenerator));

            if (stubGeneratorType != null)
            {
                StubGenerator = Activator.CreateInstance(stubGeneratorType) as IStubGenerator;
            }
            else
            {
                StubGenerator = new DefaultStubGenerator();
            }
        }

        public static dynamic For<T>()
        {
            return new InternalBuilder<T>();
        }

        public class InternalBuilder<T> : DynamicObject
        {
            private readonly Dictionary<string, object> constructorArgs = new Dictionary<string, object>();
            private IConstructorStrategy constructorStrategy = new GreediestConstructorStrategy<T>();
            private bool instanceBuilt = false;

            private BuilderResult<T> result = null; 

            internal InternalBuilder()
            {
            }

            public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
            {
                result = this;

                if (binder.Name.Equals("UsingConstructor", StringComparison.InvariantCultureIgnoreCase))
                {
                    var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
                    var typeArgs = csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>;
                    this.constructorStrategy = new ExplicitConstructorStrategy(typeof(T), typeArgs);
                }
                else
                {
                    var parameterName = this.GetParamterName("With", binder.Name);
                    this.constructorArgs.Add(parameterName, args[0]);
                }

                return true;
            }

            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                if (!instanceBuilt) throw new InvalidOperationException("Cannot access public property members until Build() has been called.");

                if (!this.result.ConstructorArgs.TryGetValue(binder.Name.ToLowerInvariant(), out result))
                {
                    return base.TryGetMember(binder, out result);
                }

                return true;
            }

            public T Build()
            {
                var ctor = this.constructorStrategy.GetConstructor(this.constructorArgs);
                var parameters = ctor.GetParameters();
                var arguments = this.BuildArgumentList(parameters).ToArray();
                var instance = (T)ctor.Invoke(arguments);

                instanceBuilt = true;

                result = new BuilderResult<T>
                {
                    Instance = instance,
                    ConstructorArgs =
                        parameters.Zip(arguments, (info, o) => new KeyValuePair<string, object>(info.Name, o))
                            .ToDictionary(x => x.Key, x => x.Value)
                };

                return instance;
            }

            private string GetParamterName(string preposition, string methodName)
            {
                if (!methodName.StartsWith(preposition, StringComparison.InvariantCultureIgnoreCase))
                {
                    throw new ArgumentException(
                        string.Format(
                            "Method name '{0}' must start with the preposition '{1}'", methodName, preposition));
                }

                return methodName.Substring(preposition.Length);
            }

            private IEnumerable<object> BuildArgumentList(IEnumerable<ParameterInfo> parameters)
            {
                var args = from param in parameters
                           join arg in this.constructorArgs on param.Name.ToLowerInvariant() equals arg.Key.ToLowerInvariant() into argGroup
                           from x in argGroup.DefaultIfEmpty()
                           select this.ResolveArgument(x, param);

                return args;
            }

            private object ResolveArgument(ConstructorArgument argument, ParameterInfo parameter)
            {
                if (!string.IsNullOrEmpty(argument.Name))
                {
                    return argument.Value;
                }

                return
                    typeof(IStubGenerator).GetMethod("GenerateStub")
                                          .MakeGenericMethod(parameter.ParameterType)
                                          .Invoke(StubGenerator, new object[0]);
            }

            private struct ConstructorArgument
            {
                public string Name { get; private set; }

                public object Value { get; private set; }

                public static implicit operator ConstructorArgument(KeyValuePair<string, object> pair)
                {
                    return new ConstructorArgument { Name = pair.Key, Value = pair.Value };
                }
            }

            private class BuilderResult<T>
            {
                public T Instance { get; set; }
                public Dictionary<string, object> ConstructorArgs { get; set; }
            }
        }
    }
}
